package pub.gll.plugin.transform;

import com.android.build.api.transform.DirectoryInput;
import com.android.build.api.transform.Format;
import com.android.build.api.transform.JarInput;
import com.android.build.api.transform.QualifiedContent;
import com.android.build.api.transform.Transform;
import com.android.build.api.transform.TransformInput;
import com.android.build.api.transform.TransformInvocation;
import com.google.common.collect.ImmutableSet;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonElement;
import com.google.gson.reflect.TypeToken;

import org.apache.commons.io.FileUtils;
import org.gradle.api.Project;

import java.io.File;
import java.io.IOException;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

import pub.gll.plugin.ClassMethodKnife;
import pub.gll.plugin.Configuration;
import pub.gll.plugin.asm.annotation.AspectCollector;
import pub.gll.plugin.asm.annotation.CollectArchBindingDataContainer;
import pub.gll.plugin.asm.model.AspectClassManager;
import pub.gll.plugin.asm.model.RuleConfigManager;
import pub.gll.plugin.asm.model.Source;
import pub.gll.plugin.asm.model.Target;
import pub.gll.plugin.extension.ClassKnifeExtension;
import pub.gll.plugin.util.ClassLoaderHelper;
import pub.gll.plugin.util.ClassUtil;
import pub.gll.plugin.util.FileUtil;
import pub.gll.plugin.util.Log;

/**
 * ClassKnife Transform
 *
 * @author yuqiang
 */
public class ClassKnifeTransform extends Transform {

    private static final String TAG = "classKnife";
    private Configuration config;
    private Gson gson;
    private ExecutorService executor = Executors.newFixedThreadPool(16);
    private Project project;

    public ClassKnifeTransform(Project project) {
        this.project = project;
    }

    @Override
    public String getName() {
        return TAG;
    }

    @Override
    public Set<QualifiedContent.ContentType> getInputTypes() {
       return ImmutableSet.of(QualifiedContent.DefaultContentType.CLASSES);

    }

    @Override
    public Set<QualifiedContent.Scope> getScopes() {
        return   ImmutableSet.of(QualifiedContent.Scope.PROJECT, QualifiedContent.Scope.SUB_PROJECTS, QualifiedContent.Scope.EXTERNAL_LIBRARIES);
    }

    @Override
    public boolean isIncremental() {
        return true;
    }

    private boolean getDebugState(TransformInvocation transformInvocation, ClassKnifeExtension extension) {
        boolean buildIsDebug = transformInvocation.getContext().getVariantName().toLowerCase().endsWith("debug");
        boolean debugConfig = extension.isDebugConfig();
        boolean debugState = extension.isDebug();
        if (!debugConfig) {
            return buildIsDebug;
        }
        return debugState;
    }

    @Override
    public void transform(TransformInvocation transformInvocation) throws InterruptedException, IOException {
        ClassKnifeExtension extension = project.getExtensions().getByType(ClassKnifeExtension.class);
        // If classKnife is disable, just copy the input folder to the output folder
        if (!extension.isEnable()) {
            copyFile(transformInvocation);
            return;
        }
        Log.getImpl().setLogLevel(extension.getLogLevel());
        config = new Configuration.Builder()
                .setEnable(extension.isEnable())
                .setDebug(getDebugState(transformInvocation, extension))
                .setExclude(extension.getExcludes())
                .setInclude(extension.getIncludes())
                .build();
        CollectArchBindingDataContainer.setConfiguration(config);
        //init classLoader
        ClassUtil.initClassLoader(ClassLoaderHelper.getClassLoader(transformInvocation.getInputs(),
                transformInvocation.getReferencedInputs(),
                project));
        long start = System.currentTimeMillis();
        try {
            doTransform(transformInvocation);
        } catch (ExecutionException e) {
            e.printStackTrace();
        }
        long cost = System.currentTimeMillis() - start;
        Log.i(getName(), "[transform] ClassKnifeTransform:%sms", cost);
    }

    private void doTransform(final TransformInvocation transformInvocation) throws ExecutionException, InterruptedException, IOException {
        //Delete output folder and reprocess files,  if not incremental compilation
        if (!transformInvocation.isIncremental()) {
            transformInvocation.getOutputProvider().deleteAll();
        }
        Log.setLogFile(project.file(project.getBuildDir() + "/outputs/classKnife/classKnife.txt"));
        Log.i(TAG, "classKnife的插入模式为: " + (config.debug ? "Debug" : "Release"));
        Log.i(TAG, "是否是增量插桩" + transformInvocation.isIncremental());
        long start = System.currentTimeMillis();
        CollectArchBindingDataContainer.reset();
        //读取上次数据
        readDataLocal(transformInvocation);
        // Step 1
        AspectCollector aspectCollector = new AspectCollector(executor);
        aspectCollector.collect(transformInvocation);
        //print rule
        dataPrint();
        Log.i(TAG, "[doTransform] AspectCollector cost:%sms", System.currentTimeMillis() - start);
        //Step 2
        start = System.currentTimeMillis();
        ClassMethodKnife methodTracer = new ClassMethodKnife(executor, config);
        methodTracer.knife(transformInvocation);
        checkArchKnife();
        Log.i(TAG, "[doTransform] ClassKnife cost:%sms", System.currentTimeMillis() - start);
    }


    private void readDataLocal(TransformInvocation transformInvocation) {
        if (transformInvocation.isIncremental()) {
            //read local rule
            String ruleLocalConfigStr = FileUtil.readFileAsString(project.getBuildDir() + "/outputs/classKnife/rule.json");
            Map<JsonElement, List<Target>> ruleLocalConfigGson = getGson().fromJson(ruleLocalConfigStr, new TypeToken<Map<JsonElement, List<Target>>>() {
            }.getType());

            Map<Source, List<Target>> ruleLocalConfig = new HashMap<>();
            ruleLocalConfigGson.entrySet().stream().forEach(stringListEntry -> {
                Source source = getGson().fromJson(stringListEntry.getKey().getAsString(), Source.class);
                ruleLocalConfig.put(source, stringListEntry.getValue());

            });
            RuleConfigManager.getInstance().setLocalConfig(ruleLocalConfig);

            //read local aspect
            String aspectClass = FileUtil.readFileAsString(project.getBuildDir() + "/outputs/classKnife/aspectClass.txt");
            String[] str = aspectClass.replace("[", "").replace("]", "").replace(" ", "").split(",");
            AspectClassManager.getInstance().clear();
            AspectClassManager.getInstance().addAllAspect(Arrays.asList(str));
        }
    }

    private void dataPrint() {
        //print rule
        if (!RuleConfigManager.getInstance().getConfig().isEmpty()) {
            File file = project.file(project.getBuildDir() + "/outputs/classKnife/rule.json");
            String json = getGson().toJson(RuleConfigManager.getInstance().getConfig());
            FileUtil.writeMsgToFile(file, json);
        }
        //print aspect
        if (!AspectClassManager.getInstance().aspect().isEmpty()) {
            File aspectFile = project.file(project.getBuildDir() + "/outputs/classKnife/aspectClass.txt");
            FileUtil.writeMsgToFile(aspectFile, AspectClassManager.getInstance().aspect().toString());
        }
    }


    private void checkArchKnife() {
        int needArchBindSize = CollectArchBindingDataContainer.needArchBindSize();
        int realArchBindSize = CollectArchBindingDataContainer.realArchBindSize();
        Log.i(TAG, "Arch框架：需要插入：" + needArchBindSize + ", 实际插入: " + realArchBindSize);
        if (!CollectArchBindingDataContainer.archBindingSizeEquals()) {
            throw new IllegalArgumentException("Arch框架：需要插入：" + needArchBindSize + ", 实际插入: " + realArchBindSize);
        }
    }

    private void copyFile(TransformInvocation transformInvocation) throws IOException {
        transformInvocation.getOutputProvider().deleteAll();
        Collection<TransformInput> inputs = transformInvocation.getInputs();
        for (TransformInput input : inputs) {
            //dir
            for (DirectoryInput directoryInput : input.getDirectoryInputs()) {
                File src = directoryInput.getFile();
                File dest = transformInvocation.getOutputProvider().getContentLocation(
                        directoryInput.getFile().getAbsolutePath(),
                        directoryInput.getContentTypes(),
                        directoryInput.getScopes(),
                        Format.DIRECTORY);
                FileUtils.forceMkdir(dest);
                FileUtil.copyFileOrDir(src, dest);
            }
            //jar
            for (JarInput inputJar : input.getJarInputs()) {
                File src = inputJar.getFile();
                File dest = transformInvocation.getOutputProvider().getContentLocation(inputJar.getFile().getAbsolutePath(),
                        inputJar.getContentTypes(),
                        inputJar.getScopes(),
                        Format.JAR);
                FileUtil.copyFileOrDir(src, dest);
            }
        }
    }


    private Gson getGson() {
        if (gson != null) {
            return gson;
        }
        gson = new GsonBuilder().disableHtmlEscaping().create();
        return gson;// Gson特殊字符转码
    }
}
